tool
extends EditorPlugin

var edited_object: Spatial = null
var my_inspector_plugin = null
var my_gizmo_plugin = null
var my_menu: Node = null

class GeneralFloorEditorAction:
	extends EditorProperty

	func setup(base_control: Control):
		var sc: ScrollContainer = ScrollContainer.new()
		sc.scroll_vertical_enabled = false
		add_child(sc)
		set_bottom_editor(sc)

		var hbc: HBoxContainer = HBoxContainer.new()
		sc.add_child(hbc)

		var buttons = [
			[null, "Add", "Add"],
			[null, "Remove", "Remove"],
			[null, "Duplicate", "Duplicate"],
			[null, "Reload", "Change Type"],
		]

		for button in buttons:
			var btn: ToolButton = ToolButton.new()
			if button[0] != null:
				btn.text = button[0]
			if button[1] != null:
				btn.icon = base_control.get_icon(button[1], "EditorIcons")
			btn.hint_tooltip = button[2]
			btn.connect("pressed", self, "_on_button_pressed", [button[2]])
			hbc.add_child(btn)

	func _on_button_pressed(value):
		emit_changed(get_edited_property(), value)

const general_flags_handles = {
	"mesh_data_editor/cube/decoration_flags": [
		{type = "flags9"},
	],
	"mesh_data_editor/cube/face_flags": [
		"Hide Face", {type = "mp_xyz"},
		"Inverted", {type = "check"},
		"Remove Prism", {type = "pm_x"},
	],
	"mesh_data_editor/cube_with_border/border_flags": [
		"Border", {
			type = "flags8",
			copy_prop = "mesh_data_editor/cube_with_border/decoration_flags",
			copy_mask = 0xFF,
			copy_offset = 0
		},
		"Inverted", {type = "flags1379"},
	],
	"mesh_data_editor/cube_with_border/decoration_flags": [
		"Decoration", {
			type = "flags8",
			copy_prop = "mesh_data_editor/cube_with_border/border_flags",
			copy_mask = 0xFF,
			copy_offset = 0
		},
	],
	"mesh_data_editor/cube_with_border/face_flags": [
		"Hide Face", {type = "mp_xyz"},
		"Remove Prism", {type = "flags1379"},
	],
}

class GeneralFloorGeneralFlags:
	extends EditorProperty

	var updating: bool = false
	var cbs: Array = []

	var prop: String

	func add_gc(p: Control, columns: int, labels: Array):
		var gc: GridContainer = GridContainer.new()
		gc.columns = columns
		p.add_child(gc)

		for s in labels:
			if s == null:
				gc.add_child(Control.new())
			else:
				var cb: ToolButton = ToolButton.new()
				cb.toggle_mode = true
				cb.text = s
				gc.add_child(cb)
				cbs.append(cb)

	func add_gc2(p: Control, columns: int, labels: Array, indices: Array):
		var gc: GridContainer = GridContainer.new()
		gc.columns = columns
		p.add_child(gc)

		var cbs2: Array = []
		for s in labels:
			var cb: ToolButton = ToolButton.new()
			cb.toggle_mode = true
			cb.text = s
			cbs.append(cb)
			cbs2.append(cb)

		for i in indices:
			if i == null:
				gc.add_child(Control.new())
			else:
				gc.add_child(cbs2[i])

	func add_control(p: Control, param: Dictionary):
		match param.type:
			"flags9":
				add_gc(p, 3, ["1", "2", "3", "4", "5", "6", "7", "8", "9"])
			"flags8":
				add_gc(p, 3, ["1", "2", "3", "4", null, "6", "7", "8", "9"])
			"flags1379":
				add_gc(p, 2, ["1", "3", "7", "9"])
			"mp_xyz":
				add_gc2(p, 3, ["-X", "+X", "-Z", "+Z", "-Y", "+Y"], \
					[null, 2, null, 0, null, 1, null, 3, null, 4, 5])
			"pm_x":
				add_gc2(p, 2, ["-X", "+X"], [1, 0])
			"check":
				var cb: CheckBox = CheckBox.new()
				p.add_child(cb)
				cbs.append(cb)
			_:
				push_error("[GeneralFloorGeneralFlags::add_control] Unknown type: '%s'" % param.type)

	func add_copy_btn(p: Control, base_control: Control, param: Dictionary):
		if "copy_prop" in param:
			var btn: ToolButton = ToolButton.new()
			btn.icon = base_control.get_icon("ActionPaste", "EditorIcons")
			btn.hint_tooltip = "Copy from %s" % param.copy_prop
			p.add_child(btn)
			btn.connect("pressed", self, "_on_copy_pressed", [param])
			p.add_child(Control.new())

	func setup(base_control: Control):
		var arr: Array = general_flags_handles[prop]

		if arr.size() >= 2:
			var gc: GridContainer = GridContainer.new()
			gc.columns = 2
			add_child(gc)
			set_bottom_editor(gc)

			var i: int = 0
			while i + 1 < arr.size():
				var lbl: Label = Label.new()
				lbl.size_flags_vertical = 0
				lbl.text = arr[i]
				gc.add_child(lbl)
				add_copy_btn(gc, base_control, arr[i + 1])
				add_control(gc, arr[i + 1])
				i += 2

		else:
			add_control(self, arr[0])

		for i in cbs.size():
			cbs[i].connect("toggled", self, "_on_button_toggled", [i])

	func _on_button_toggled(pressed: bool, index: int):
		if not updating:
			var new_value: int = get_edited_object()[get_edited_property()]
			if pressed:
				new_value |= (1 << index)
			else:
				new_value &= ~(1 << index)
			emit_changed(get_edited_property(), new_value)

	func _on_copy_pressed(param: Dictionary):
		var src_value: int = get_edited_object()[param.copy_prop]
		src_value = (src_value & param.copy_mask) << param.copy_offset
		var new_value: int = get_edited_object()[get_edited_property()]
		new_value &= ~(param.copy_mask << param.copy_offset)
		emit_changed(get_edited_property(), new_value | src_value)

	func update_property():
		var new_value: int = get_edited_object()[get_edited_property()]
		updating = true
		for i in cbs.size():
			cbs[i].pressed = (new_value & (1 << i)) != 0
		updating = false

class MyInspectorPlugin:
	extends EditorInspectorPlugin

	var base_control: Control = null

	func can_handle(object):
		return object is GeneralFloor

	func parse_property(object, type, path, hint, hint_text, usage):
		var ctl = null
		if path.ends_with("_editor/action"):
			ctl = GeneralFloorEditorAction.new()
		elif path in general_flags_handles:
			ctl = GeneralFloorGeneralFlags.new()
			ctl.prop = path
		if ctl != null:
			ctl.setup(base_control)
			add_property_editor(path, ctl)
			return true
		return false

class MyCustomGizmoPlugin:
	extends EditorSpatialGizmoPlugin

	var edit_mode: String = ""
	var handles: Array = []
	var undo_redo: UndoRedo

	func get_name():
		return "GeneralFloor"

	func has_gizmo(spatial):
		return spatial != null and spatial is GeneralFloor

	func setup(p: EditorPlugin):
		undo_redo = p.get_undo_redo()

		create_material("main", Color(1.0, 1.0, 0.5))
		create_handle_material("handles")

		var mats = [
			["x", Color(1.0, 0.2, 0.2)],
			["y", Color(0.2, 1.0, 0.2)],
			["z", Color(0.2, 0.2, 1.0)],
		]
		for p in mats:
			var sm: SpatialMaterial = SpatialMaterial.new()
			sm.flags_unshaded = true
			sm.flags_do_not_receive_shadows = true
			sm.flags_no_depth_test = true
			sm.albedo_color = p[1]
			add_material(p[0], sm)

	func get_handle_name(gizmo, index):
		return handles[index].name

	func get_handle_value(gizmo, index):
		var obj: Spatial = gizmo.get_spatial_node()

		if obj == null or not obj is GeneralFloor:
			return

		var idx = obj._get_editor_index()
		if not (idx >= 0 and idx < obj.meshes.size()):
			return

		var type: String = obj.mesh_data[idx].type
		var prop_subnames: Array = []

		match type:
			"cube":
				# TODO:
				pass

			"cube_with_border":
				match edit_mode:
					"extent":
						if (index & 1) == 0:
							prop_subnames.append("origin")
						prop_subnames.append("extent")
					"slope":
						if (index & 1) == 0:
							if (index & 0xC) != 0:
								prop_subnames.append("origin")
							if (index & 0xC) != 4:
								prop_subnames.append("extent")
						if (index & 0xC) != 8:
							prop_subnames.append("slope")
						if (index & 0xC) != 4:
							prop_subnames.append("bottom_slope")

		return {} if prop_subnames.empty() else obj._dict_editor.get_multiple_props(obj, "mesh_data_editor/" + type, prop_subnames)

	func set_handle(gizmo, index, camera, point):
		var obj: Spatial = gizmo.get_spatial_node()

		if obj == null or not obj is GeneralFloor:
			return

		var idx = obj._get_editor_index()
		if not (idx >= 0 and idx < obj.meshes.size()):
			return

		var xf: Transform = obj.global_transform
		var p1: Vector3 = camera.project_ray_origin(point)
		var p2: Vector3 = p1 + camera.project_ray_normal(point) * 20000.0
		p1 = xf.xform_inv(p1)
		p2 = xf.xform_inv(p2)

		var type: String = obj.mesh_data[idx].type

		match type:
			"cube":
				# TODO:
				pass

			"cube_with_border":
				var old_prop: Dictionary = obj._dict_editor.get_multiple_props(obj, "mesh_data_editor/" + type, \
					["origin", "extent", "slope", "bottom_slope"])
				var origin: Vector3 = old_prop.origin
				var extent: Vector3 = old_prop.extent
				var slope: Vector2 = old_prop.slope
				var bottom_slope: Vector2 = old_prop.bottom_slope

				var new_prop: Dictionary = {}

				match edit_mode:
					"extent":
						var ii: int = index & ~1
						var q1: Vector3 = handles[ii].pos
						var q2: Vector3 = handles[ii + 1].pos
						var qq: Vector3 = Geometry.get_closest_points_between_segments(p1, p2, \
							q1 - (q2 - q1) * 20000.0, q2 + (q2 - q1) * 20000.0)[1]

						var step: float = 1.0

						var new_origin: Vector3 = origin
						var new_extent: Vector3 = extent

						ii = index >> 1

						if (index & 1) != 0:
							new_extent[ii] = stepify(max(qq[ii] - q1[ii], step), step)
						else:
							new_extent[ii] = stepify(max(q2[ii] - qq[ii], step), step)
							var dd: float = extent[ii] - new_extent[ii]
							if ii != 1:
								new_origin.y += dd * (slope + bottom_slope)[ii >> 1]
								new_extent.y -= dd * bottom_slope[ii >> 1]
							new_origin[ii] += dd

						if not origin.is_equal_approx(new_origin):
							new_prop.origin = new_origin
						if not extent.is_equal_approx(new_extent):
							new_prop.extent = new_extent

					"slope":
						var q1: Vector3 = handles[index].pos
						var qq: Vector3 = Geometry.get_closest_points_between_segments(p1, p2, \
							q1 - Vector3(0, 20000.0, 0), q1 + Vector3(0, 20000.0, 0))[1]

						var step: float = 0.125
						var slp: float = stepify((qq.y - q1.y) / extent[index & 2] * \
							(1.0 if (index & 1) != 0 else -1.0), step)

						if abs(slp) > 1e-3:
							match index & 0xC:
								0:
									slope[(index & 2) >> 1] += slp
									bottom_slope[(index & 2) >> 1] -= slp
									new_prop.slope = slope
									new_prop.bottom_slope = bottom_slope
									if (index & 1) == 0:
										extent.y -= slp * extent[index & 2]
										new_prop.extent = extent
								4:
									slope[(index & 2) >> 1] += slp
									new_prop.slope = slope
									if (index & 1) == 0:
										origin.y -= slp * extent[index & 2]
										new_prop.origin = origin
								8:
									bottom_slope[(index & 2) >> 1] += slp
									new_prop.bottom_slope = bottom_slope
									if (index & 1) == 0:
										origin.y -= slp * extent[index & 2]
										extent.y += slp * extent[index & 2]
										new_prop.origin = origin
										new_prop.extent = extent

				if not new_prop.empty():
					obj._dict_editor.set_multiple_props(obj, "mesh_data_editor/" + type, new_prop)

	func commit_handle(gizmo, index, restore, cancel = true):
		var obj: Spatial = gizmo.get_spatial_node()

		if obj == null or not obj is GeneralFloor:
			return

		var idx = obj._get_editor_index()
		if not (idx >= 0 and idx < obj.meshes.size()):
			return

		var type: String = obj.mesh_data[idx].type

		if cancel:
			obj._dict_editor.set_multiple_props(obj, "mesh_data_editor/" + type, restore)
		else:
			undo_redo.create_action("Change %s %s %s" % [type, edit_mode, handles[index].name])
			var new_prop: Dictionary = obj._dict_editor.get_multiple_props(obj, "mesh_data_editor/" + type, restore)
			undo_redo.add_do_method(obj._dict_editor, "set_multiple_props", obj, "mesh_data_editor/" + type, new_prop)
			undo_redo.add_undo_method(obj._dict_editor, "set_multiple_props", obj, "mesh_data_editor/" + type, restore)
			undo_redo.commit_action()

		obj.property_list_changed_notify()

	func draw_handles(gizmo: EditorSpatialGizmo, obj: Spatial, type: String):
		match type:
			"cube":
				# TODO:
				pass

			"cube_with_border":
				var pts: PoolVector3Array = PoolVector3Array()

				var old_prop: Dictionary = obj._dict_editor.get_multiple_props(obj, "mesh_data_editor/" + type, \
					["origin", "extent", "slope", "bottom_slope"])
				var origin: Vector3 = old_prop.origin
				var extent: Vector3 = old_prop.extent
				var slope: Vector2 = old_prop.slope
				var bottom_slope: Vector2 = old_prop.bottom_slope

				var additional_lines: Array = []

				match edit_mode:
					"extent":
						handles = [
							{name = "-X", pos0 = Vector3(0, 0.5, 0.5)},
							{name = "+X", pos0 = Vector3(1, 0.5, 0.5)},
							{name = "-Y", pos0 = Vector3(0.5, 0, 0.5)},
							{name = "+Y", pos0 = Vector3(0.5, 1, 0.5)},
							{name = "-Z", pos0 = Vector3(0.5, 0.5, 0)},
							{name = "+Z", pos0 = Vector3(0.5, 0.5, 1)},
						]
						additional_lines = [
							{mat = "x", idx = [0, 1]},
							{mat = "y", idx = [2, 3]},
							{mat = "z", idx = [4, 5]},
						]
					"slope":
						handles = [
							{name = "top_slope -X", pos0 = Vector3(0, 1, 0.5)},
							{name = "top_slope +X", pos0 = Vector3(1, 1, 0.5)},
							{name = "top_slope -Z", pos0 = Vector3(0.5, 1, 0)},
							{name = "top_slope +Z", pos0 = Vector3(0.5, 1, 1)},
							{name = "slope -X", pos0 = Vector3(0, 0.5, 0.5)},
							{name = "slope +X", pos0 = Vector3(1, 0.5, 0.5)},
							{name = "slope -Z", pos0 = Vector3(0.5, 0.5, 0)},
							{name = "slope +Z", pos0 = Vector3(0.5, 0.5, 1)},
							{name = "bottom_slope -X", pos0 = Vector3(0, 0, 0.5)},
							{name = "bottom_slope +X", pos0 = Vector3(1, 0, 0.5)},
							{name = "bottom_slope -Z", pos0 = Vector3(0.5, 0, 0)},
							{name = "bottom_slope +Z", pos0 = Vector3(0.5, 0, 1)},
						]
						additional_lines = [
							{mat = "x", idx = [0, 1, 4, 5, 8, 9]},
							{mat = "z", idx = [2, 3, 6, 7, 10, 11]},
						]

				for param in handles:
					var v: Vector3 = param.pos0
					param.pos = Vector3(origin.x + extent.x * v.x, \
						origin.y + extent.y * v.y \
						+ (slope.x + bottom_slope.x * (1.0 - v.y)) * extent.x * v.x \
						+ (slope.y + bottom_slope.y * (1.0 - v.y)) * extent.z * v.z, \
						origin.z + extent.z * v.z)
					pts.append(param.pos)

				if pts.size() > 0:
					gizmo.add_handles(pts, get_material("handles", gizmo))

				for param in additional_lines:
					pts = PoolVector3Array()
					for i in param.idx:
						pts.append(handles[i].pos)
					gizmo.add_lines(pts, get_material(param.mat, gizmo))

	func redraw(gizmo):
		gizmo.clear()
		handles = []

		var obj = gizmo.get_spatial_node()

		if obj == null:
			pass
		elif obj is GeneralFloor:
			var idx = obj._get_editor_index()
			if idx >= 0 and idx < obj.meshes.size():
				# draw bounding box
				var mesh: Mesh = obj.meshes[idx].mesh
				var aabb: AABB = mesh.get_aabb().grow(0.1)
				var v: PoolVector3Array = PoolVector3Array()
				for x in 2:
					for y in 2:
						for z in 2:
							var v0: Vector3 = aabb.position + Vector3(aabb.size.x * x, aabb.size.y * y, aabb.size.z * z)
							var v1: Vector3 = v0.linear_interpolate(aabb.position + aabb.size * 0.5, 0.6)
							v.append(v0)
							v.append(Vector3(v1.x, v0.y, v0.z))
							v.append(v0)
							v.append(Vector3(v0.x, v1.y, v0.z))
							v.append(v0)
							v.append(Vector3(v0.x, v0.y, v1.z))
				gizmo.add_lines(v, get_material("main", gizmo))

				# draw handles
				draw_handles(gizmo, obj, obj.mesh_data[idx].type)

class MyMenu:
	extends HBoxContainer

	var mode_btn: MenuButton

	signal menu_pressed(param)

	func _on_mode_index_pressed(index):
		var popup: PopupMenu = mode_btn.get_popup()
		mode_btn.text = "Mode: %s" % popup.get_item_text(index)
		for i in popup.get_item_count():
			if popup.is_item_radio_checkable(i):
				popup.set_item_checked(i, i == index)
		emit_signal("menu_pressed", {
			index = index,
			popup = popup,
		})

	func setup(base_control: Control):
		var obj = VSeparator.new()
		add_child(obj)

		mode_btn = MenuButton.new()
		mode_btn.text = "Mode"
		add_child(mode_btn)

		var popup: PopupMenu = mode_btn.get_popup()

		for p in [
			["none", KEY_Z],
			["extent", KEY_X],
			["slope", KEY_C],
		]:
			popup.add_radio_check_item(p[0], -1, p[1])

		popup.connect("index_pressed", self, "_on_mode_index_pressed")

func _on_menu_pressed(param):
	var s: String = param.popup.get_item_text(param.index)
	my_gizmo_plugin.edit_mode = s
	if edited_object != null:
		edited_object.update_gizmo()

func _enter_tree():
	var base_control: Control = get_editor_interface().get_base_control()
	my_inspector_plugin = MyInspectorPlugin.new()
	my_inspector_plugin.base_control = base_control
	add_inspector_plugin(my_inspector_plugin)

	my_gizmo_plugin = MyCustomGizmoPlugin.new()
	my_gizmo_plugin.setup(self)
	add_spatial_gizmo_plugin(my_gizmo_plugin)

	my_menu = MyMenu.new()
	my_menu.setup(base_control)
	my_menu.connect("menu_pressed", self, "_on_menu_pressed")
	add_control_to_container(CONTAINER_SPATIAL_EDITOR_MENU, my_menu)

	make_visible(false)

func _exit_tree():
	remove_control_from_container(CONTAINER_SPATIAL_EDITOR_MENU, my_menu)
	my_menu.queue_free()

	remove_inspector_plugin(my_inspector_plugin)
	my_inspector_plugin = null

	remove_spatial_gizmo_plugin(my_gizmo_plugin)
	my_gizmo_plugin = null

func handles(object):
	return object is GeneralFloor

func make_visible(visible):
	my_menu.visible = visible
	my_menu.set_process(visible)

func edit(object):
	if object == null or object is GeneralFloor:
		edited_object = object
	else:
		push_error("ERROR: Class '%s' is unrecognized for custom editor" % object.get_class())

func clear():
	edit(null)
	make_visible(false)

func forward_spatial_gui_input(camera, event):
	if edited_object == null:
		pass
	elif edited_object is GeneralFloor:
		if event is InputEventMouseButton and event.pressed and event.button_index == BUTTON_LEFT:
			var point: Vector2 = event.position

			if edited_object.gizmo != null and \
					edited_object.gizmo is EditorSpatialGizmo and \
					edited_object.gizmo.get_plugin() == my_gizmo_plugin:
				var xf: Transform = edited_object.global_transform
				for param in my_gizmo_plugin.handles:
					var p: Vector2 = camera.unproject_position(xf.xform(param.pos))
					if (point - p).length_squared() < 72.0:
						return false

			var from: Vector3 = camera.project_ray_origin(point)
			var normal: Vector3 = camera.project_ray_normal(point)
			var planes: Array = camera.get_frustum()

			for sb in edited_object.areas:
				sb.collision_layer = 0x10000000
				sb.collision_mask = 0x10000000

			var pdss: PhysicsDirectSpaceState = edited_object.get_world().direct_space_state
			var ret: Dictionary = pdss.intersect_ray(from, from + normal * camera.far, \
				[], 0x10000000)

			for sb in edited_object.areas:
				sb.collision_layer = 0
				sb.collision_mask = 0

			if ret.empty():
				return false

			var inters: Vector3 = ret.get("position")
			var collider: Spatial = ret.get("collider")

			# Make sure the intersection is inside the frustum planes, to avoid
			# painting on invisible regions.
			for pl in planes:
				if (pl as Plane).is_point_over(inters):
					return false

			if edited_object._editor_clicked(collider):
				return true

	return false
